createBridge = function(ground, start, end, 
                        segments, height) {
    segments = segments || 10;
    height = height || 5;

    start.y = ground.getHeight(start.z, start.x);
    end.y   = ground.getHeight(end.z, end.x);

    var dir = end.sub(start).scaleMe(1 / segments); 
    var bb = new SolidBuilder();

    var B; // "bellshape", 0 to 1 to 0
    for (var i = 0; i <= segments; i++) {
        B = i*i*(segments-i)*(segments-i) / (Math.pow(segments,4)/16);
        bb.add( start.add(dir.scale(i)).addMe( Vec(0,height*B,0)),
                12 + 80*(1-B), 2 + 14*(1-B) );
    }

    return bb.create();
};

createTunnel = function(ground, start, end) {

    var width = 25;
    var height = 20;
    var thickness = 5;
    
    var segments = 15;
    var slope = 45;

    start.y = ground.getHeight(start.z, start.x) + height;
    end.y   = ground.getHeight(end.z, end.x) + height;
    var dir = end.sub(start).scaleMe(1 / segments); 

    var right = dir.cross(Vec(0,1,0)).normalizeMe();


    var startleft = start.sub(right.scale(width/2 + thickness/2));
    var startright = start.add(right.scale(width/2 + thickness/2));
    var starttop = start.add(Vec(0, thickness, 0));
    var startbottom = start.add(Vec(0, - height, 0));
    
    var c = new SolidBuilder(true);
    var l = new SolidBuilder();
    var r = new SolidBuilder();
    var t = new SolidBuilder();
    var b = new SolidBuilder();

    var B; // "bellshape", 0 to 1 to 0
    for (var i = 0; i <= segments; i++) {
        B = i*i*(segments-i)*(segments-i) / (Math.pow(segments,4)/16);
        
        var vec = dir.scale(i).addMe( Vec(0,-slope*B,0));

        b.add( startbottom.add(vec), width + 2*thickness, thickness );
        t.add( starttop.add(vec), width + 2*thickness, thickness );
        r.add( startright.add(vec), thickness, height );
        l.add( startleft.add(vec), thickness, height );
        c.add( start.add(vec).add(Vec(0,thickness,0)), width + 2*thickness, height + 2*thickness );
    }

    var solids = c.create(true).concat(l.create()).concat(r.create()).concat(t.create()).concat(b.create());
    return solids;
};

SolidBuilder = function(isHollow) {
    this.isHollow = isHollow;
    this.path = [];
};
SolidBuilder.prototype.add = function(pos, width, thickness) {
    this.path.push({'pos': pos,
                    'thickness': thickness,
                    'width': width});
};
SolidBuilder.prototype._setDirections = function() {
    var prev = this.path[0];
    for (var i = 1; i < this.path.length; i++) {
        prev.dir = this.path[i].pos.sub(prev.pos).normalizeMe();
        prev = this.path[i];
    }
    // prev is last                                                             
    prev.dir = this.path[this.path.length - 2].dir;

};
SolidBuilder.prototype._calcExpansion = function(prev, curr) {
    return curr.dir.add(prev.dir).cross( Vec(0,1,0) ).normalizeMe();
};
SolidBuilder.prototype._setExpansions = function() {
    var prev = this.path[0];
    for (var i = 0; i < this.path.length; i++) {
        var curr = this.path[i];
        curr.exp = this._calcExpansion(prev, curr);
        prev = curr;
    }
};
SolidBuilder.prototype._setCrossCuts = function() {
    this.crossCuts = [];                           
    for (var i = 0; i < this.path.length; i++) {
        var p = this.path[i];
        var p0 = p.pos.add( p.exp.scale(p.width / 2) ); // "right"              
        var p1 = p0.add( Vec(0,-1,0).scale(p.thickness) ); // then "down"          
        var p2 = p1.add( p.exp.scale(- p.width) );      // then "left"          
        var p3 = p2.add( Vec(0,1,0).scale(p.thickness) );  // then "up"            
        this.crossCuts.push( CrossCut(p0, p1, p2, p3) );
    }
};
SolidBuilder.prototype._setSolids = function() {
    this.solids = [];
    var prev = this.crossCuts[0];
    for (var i = 1; i < this.crossCuts.length; i++) {
        var curr = this.crossCuts[i];
        this.solids.push( new Solid(prev, curr, this.isHollow) );
        prev = curr;
    }
};
SolidBuilder.prototype._adjustSides = function() {
    // If not adjusting sides, the positions defining                           
    // 'top' resp. 'bottom' sides might not lie in one plane,                   
    // which is assumed by physics                                              
    var prev = this.crossCuts[0];
    var curr, n, avg;
    for (var i = 1; i < this.crossCuts.length; i++) {
        curr = this.crossCuts[i];
	    curr.adjustToPrevious(prev);
        prev = curr;
    }
};
SolidBuilder.prototype.create = function() {
    this._setDirections();
    this._setExpansions();
    this._setCrossCuts();
    this._adjustSides();
    this._setSolids();
    return this.solids;
};

// A cross-cut of 4 points
CrossCut = function(p0, p1, p2, p3) {
    if (!(this instanceof CrossCut))
	return new CrossCut(p0, p1, p2, p3);
    this.rightup = p0;
    this.rightdown = p1;
    this.leftdown = p2;
    this.leftup = p3;

};
CrossCut.prototype.validateAssumptions = function() {
    var assumedup = this.rightup.sub( this.rightdown ).normalizeMe();
    var assumedepsilon = assumedup.subMe( Vec(0,1,0) ).length();
    if (assumedepsilon > 0.01)
	throw "CrossCut does not fulfill assumptions";
};
CrossCut.prototype.adjustToPrevious = function(prev) {
    this.validateAssumptions();
    prev.validateAssumptions();
    var avg, n;

    // top                                                                  
    avg = this.rightup.add(this.leftup).scaleMe(1/2);
    n = prev.rightup.sub(prev.leftup).cross( avg.sub(prev.leftup) ).normalizeMe();
    this.rightup.subMe(n.scale(this.rightup.dot(n) - avg.dot(n)));
    this.leftup.subMe(n.scale(this.leftup.dot(n) - avg.dot(n)));
    
    // bottom                                                               
    avg = this.rightdown.add(this.leftdown).scaleMe(1/2);
    n = prev.rightdown.sub(prev.leftdown).cross( avg.sub(prev.leftdown) ).normalizeMe();
    this.rightdown.subMe(n.scale(this.rightdown.dot(n) - avg.dot(n)));
    this.leftdown.subMe(n.scale(this.leftdown.dot(n) - avg.dot(n)));
};

CrossCut.prototype.translate = function(d) {
    return new CrossCut(this.rightup.add(d), this.rightdown.add(d), this.leftdown.add(d), this.leftup.add(d));
};

BoundingBox = function( solids ) {

    // Setup of a bounding rectangle for broad phase collision detection.
    // Assumption: If the rectangle covers first and last solid
    //             then all solids are covered - will be verified in runtime.

    var centerOfPhy = function( phyObj ) {

        var count = 0;
        var center = Vec( 0, 0, 0 );

        phyObj.eachPoint( function( p ) {

            count ++;
            center.addMe( p.pos );

        });

        return center.scaleMe( 1 / count );

    }

    var coverRadius = function( phyObj, center ) {

        var r = 0;

        phyObj.eachPoint( function( p ) {

            r = Math.max( r, p.pos.sub( center ).length() );

        });

        return r;
    }
    
    var firstPhyObj = solids[ 0 ].getPhyObj();
    var lastPhyObj = solids[ solids.length - 1 ].getPhyObj();

    var from = centerOfPhy( firstPhyObj );
    var to = centerOfPhy( lastPhyObj )
    var up = Vec( 0, 1, 0 );
    
    var dir = to.sub( from ).normalizeMe();
    var right = dir.cross( up ).normalizeMe()
    var r = Math.max( coverRadius( firstPhyObj, from ), coverRadius( lastPhyObj, to ) );

    this.faces = [ { pos : from.sub( dir.scale( r ) ),   normal : dir.scale( -1 ) },
                   { pos : from.add( right.scale( r ) ), normal : right },
                   { pos : from.sub( right.scale( r ) ), normal : right.scale( -1 ) },
                   { pos : to.add( dir.scale( r ) ),     normal : dir } ];

    // Verify that all solids are contained

    var box = this;

    for ( var i = 0, length = solids.length; i < length; i++ ) {
        
        solids[ i ].getPhyObj().eachPoint( function( point ) {

            if ( ! box.contains( point.pos ) ) 

                console.log( "Bounding box does not contain all points!" );

        });

    }

};    

BoundingBox.prototype = {

    contains : function( vector ) {

        for ( var i = 0, length = this.faces.length; i < length; i++ ) {

            var face = this.faces[ i ];

            if ( vector.sub( face.pos ).dot( face.normal ) > 0 ) {

                return false;

            }

        }

        return true;

    }

};

CompositeSolidPhyObj = function( solids ) {

    this.solids = solids;
    this.boundingBox = new BoundingBox( solids );

};

CompositeSolidPhyObj.prototype = {

    testCollision : function( pos, onCollision ) {

        // Broad phase

        if ( ! this.boundingBox.contains( pos ) ) {

            return;

        }

        // Narrow phase

        for ( var i = 0; i < this.solids.length; i++ ) {

            var solid = this.solids[ i ];
            var phyObj = solid.getPhyObj();

            phyObj.testCollision( pos, function( normal, penetration ) {

                onCollision( normal, penetration, solid.isHollow );

            });

        };

    },

    closestPos : function( pos ) {

        // For example, used for sewing ground to tunnels.
        // Most of the time it will not be exact!

        var theclosest = null;
        var mindist = 100000;

        for ( var i = 0, length = this.solids.length; i < length; i++ ) {
            
            var p = this.solids[ i ].getPhyObj().closestPos( pos );
            var dist = p.sub( pos ).length();

            if ( dist < mindist ) {

                theclosest = p;
                mindist = dist;

            }
        }

        return theclosest;
    
    },

    eachPoint : function(callback) {

        for (var i = 0; i < this.solids.length; i++) {

            this.solids[i].getPhyObj().eachPoint(callback);

        }
    },

    satisfyConstraints : function() { },

    verletIntegrate : function(dt) { },

    isSolid : function() { return true; },

    clone : function() {

        throw "NOT IMPLEMENTED: Will these objects be forever static? Naah..?";
    
    }

};

CompositeSolid = function(solids, entModel) {
    if (!(this instanceof CompositeSolid))
        return new CompositeSolid(solids, entModel);

    BaseEntity.apply(this);
    this.solids = solids;
    
    if (entModel) {
	this.entModel = entModel;
	if (entModel instanceof TelePortal) {
	    this.isPortal = true;
	}
    }
    
    this.phyObj = new CompositeSolidPhyObj(solids);
};

CompositeSolid.prototype = Object.create(BaseEntity.prototype);

// Solid is a 6-sided convex body, e.g. a stretched cube.                    

Solid = function(crosscut0, crosscut1, isHollow, isPortal) {
    if (!(this instanceof Solid))
	   return new Solid(crosscut0, crosscut1);

    BaseEntity.apply(this);

    this.isHollow = isHollow;
    this.isPortal = isPortal;
    this.isSolid = true;

    var vec2Point = function(vec) { var p = Point(); p.pos.set(vec); return p;};

    var points = [ vec2Point( crosscut0.rightdown ), // bottom
		   vec2Point( crosscut1.rightdown ),
		   vec2Point( crosscut1.leftdown ),
		   vec2Point( crosscut0.leftdown ),
		   vec2Point( crosscut0.rightup ), // top
		   vec2Point( crosscut1.rightup ),
		   vec2Point( crosscut1.leftup ),
		   vec2Point( crosscut0.leftup ) ]

    this.phyObj = PhyObj( points,
                          [],
                          [[0,3,2, 1], // bottom                                
                           [4,5,6, 7], // top                                   
                           [0,1,5, 4], // this way                              
                           [3,7,6, 2], // other way                             
                           [1,2,6, 5], // right                                 
                           [0,4,7, 3]] ); // left                               
};

Solid.prototype = Object.create(BaseEntity.prototype);

/**
 *
 */
function TelePortal(pos, onGround, fwdNorm) {
    
    this.pos = pos;
    this.onGround = onGround;    
    this.forwardNormal = fwdNorm;
    
    this.forwardPortal = null;
    this.backwardPortal = null;
    
    this.solids = this._createSolids();
}

TelePortal.prototype = {
    
    _createSolids: function() {
	
	var solids = [];
	
	var pos = this.pos;
	var fwd = this.forwardNormal;
	var up = Vec(0, 1, 0);
	var right = up.cross(fwd);
	
	var width = 15;
	var height = 15;
	var thickness = 5;
	
	var tl = pos.add(up.scale(height)).sub(right.scale(width / 2));
	var tr = pos.add(up.scale(height)).add(right.scale(width / 2));
	var bl = pos.sub(right.scale(width / 2));
	var br = pos.add(right.scale(width / 2));
	
	var thd = fwd.scale(-thickness);
	var thr = right.scale(thickness);
	var thu = up.scale(thickness);
	
	//CrossCuts
	var c1 = CrossCut(tr, br, bl, tl);
	var c2 = c1.translate(thd);
	
	var l1 = CrossCut(tl, bl, bl.sub(thr), tl.sub(thr));
	var l2 = l1.translate(thd);
	
	var r1 = CrossCut(tr.add(thr), br.add(thr), br, tr);
	var r2 = r1.translate(thd);

	var t1 = CrossCut(tr.add(thr).add(thu), tr.add(thr), tl.sub(thr), tl.sub(thr).add(thu));
	var t2 = t1.translate(thd);
	
	solids.push(new Solid(c1, c2, true, true));
	solids.push(new Solid(l1, l2, false));
	solids.push(new Solid(r1, r2, false));
	solids.push(new Solid(t1, t2, false));
	
	//if (!this.onGround) {
	//    var b1 = CrossCut(tr, br, bl, tl);
	//    var b2 = CrossCut(tr.add(d), br.add(d), bl.add(d), tl.add(d));
	//}
	
    	return solids;
    },
    
    setForwardPortal: function(portal) {
	this.forwardPortal = portal;
    },
    
    setBackwardPortal: function(portal) {
	this.backwardPortal = portal;
    },
    
    getForwardPortal: function() {
	return this.forwardPortal;
    },
    
    getBackwardPortal: function() {
	return this.backwardPortal;
    },
    
    getForwardNormal: function() {
	return this.forwardNormal;
    },
    
    getPos: function() {
	return this.pos;
    }
    
};




